#include "CmdLineParser.h"
#include "../collections/KeyValueListT.h"
#include "../common/UtilFuncT.h"

using namespace cppflib::collections;

namespace cppflib
{

namespace util
{


/**
 *  Make an array of CFString
 */
PRIVATE ArrayCppObjT<CFString>* __Convert(int argc, char * argv[])
{
   ArrayCppObjT<CFString> *pArray = new ArrayCppObjT<CFString>(argc);
   for (int i = 0; i < argc; ++i)
      pArray->Set(i, new CFString(static_cast<const char *>(argv[i])));
   return pArray;
}

/**
 *  Make an array of CFString
 */
PRIVATE ArrayCppObjT<CFString>* __Convert(int argc, pwstr_t argv[])
{
   ArrayCppObjT<CFString> *pArray = new ArrayCppObjT<CFString>(argc);
   for (int i = 0; i < argc; ++i)
      pArray->Set(i, new CFString(static_cast<pcwstr_t>(argv[i])));
   return pArray;
}


/**
 *  Make a hash table of options with one extra argument
 *
 *  @param [in] optWithArg -- list of options with an extra argument
 *                            
 */
PRIVATE HashtableT<CFString, CFString> * __ConvertOptionsWithArg(pcwstr_t optWithArg[])
{
    int i = 0;
    HashtableT<CFString, CFString> * pTbl = new HashtableT<CFString, CFString>();

    if (optWithArg) {
       while (optWithArg[i]) {
          pTbl->Set(new CFString(optWithArg[i]), new CFString(_S("")));  // value is ignored, let it empty
          ++i;
       }
    }

    return pTbl;
}


/**
 *  Parse a string which may or may not be a key-value string
 *
 *  Single key should be XXXX.
 *  Key-value pair should be XXXX=YYYY or XXXX="aaa hhh eee" 
 *
 *  @param [in] src -- source string
 *  @param [out] key -- key part
 *  @param [out] value -- value part (empty string but not NULL for key-only)
 */
PRIVATE void __ParseKeyValue(CFString& src, CFString*& pKey, CFString*& pVal)
{
    bool parsingKeyPart = true;
    bool valStartWithQuote = false;
    int keyStart = -1, keyEnd = -1, valStart = -1, valEnd = -1;  
    for (int i = 0; i < src.GetLength(); ++i) {
       wc_t c = src[i]; 

       if (parsingKeyPart) {
          // parsing key
          if (c == _S('=')) { 
             keyEnd = i - 1;
             parsingKeyPart = false;
          }
          else {
             if (keyStart < 0) keyStart = i;
          }
       }
       else {
          // parsing value
          if (c == _S('"')) {
             if (valStart < 0) {
                valStart = i + 1;
                valStartWithQuote = true;
             }
             else {
                if (valStartWithQuote) {
                   valEnd = i - 1;
                   break;
                }
             }
          }
          else {
             if (valStart < 0)
                valStart = i;
          }
       }
    }

    if (keyStart >= 0 && keyEnd < 0)
       keyEnd = src.GetLength() - 1;  // may not set

    if (valStart >= 0 && valEnd < 0)
       valEnd = src.GetLength() - 1;  // may not set

    // assign result
    if (keyStart >= 0 && keyEnd >= 0 && (keyEnd - keyStart) > 0)
       pKey = new CFString(src.Mid(keyStart, keyEnd - keyStart + 1));

    if (valStart >= 0 && valEnd >= 0 && (valEnd - valStart) > 0)
       pVal = new CFString(src.Mid(valStart, valEnd - valStart + 1));

    if (!pKey) pKey = new CFString(_S(""));  // just in case
    if (!pVal) pVal = new CFString(_S(""));  // just in case

}

/**
 *  Add option to table
 *
 *  @param [in] opt -- target option
 *  @param [in] arg -- argument of the option (may be empty or single key or key-value pair)
 */
PRIVATE void __AddOptToTable(LinkedHashtableT<CFString, BaseObject>& argTable,
                             CFString& opt, CFString& arg)
{
   CFString *pKey = NULL, *pVal = NULL;

   if (!arg.IsEmpty())
      __ParseKeyValue(arg, pKey, pVal);

   // only value is available
   if (CFString::IsNullOrEmpty(pKey) && !CFString::IsNullOrEmpty(pVal))
      return;
         
   if (argTable.ContainsKey(opt)) {
      // already has the same option
      NameValueList *pEmbeddedTable = NULL;
      BaseObject *pOrgVal = argTable.Get(opt);
      
      if (cppflib::InstanceOf<CFString>(pOrgVal)) {
         pEmbeddedTable = new NameValueList();
         pEmbeddedTable->AddTail(static_cast<CFString*>(pOrgVal), new CFString(_S("")));  // add to embedded table
         argTable.Set(&opt, pEmbeddedTable);    // replace the original one
      }
      else {
         pEmbeddedTable = static_cast<NameValueList*>(pOrgVal);
      }

      pEmbeddedTable->AddTail(pKey, pVal); // add new one

   }
   else {
      // new option
      if (CFString::IsNullOrEmpty(pKey) && CFString::IsNullOrEmpty(pVal)) {
         argTable.Set(&opt, new CFString(_S("")));   // no key and no value
      }
      else if (CFString::IsNullOrEmpty(pVal)) {
         argTable.Set(&opt, pKey);               // no value
      }
      else {
         NameValueList *pEmbeddedTable = new NameValueList();
         pEmbeddedTable->AddTail(pKey, pVal);
         argTable.Set(&opt, pEmbeddedTable);       // table in table
      }
   } 
}

CmdLineParser::CmdLineParser(void)
{
   progName = NULL;
   pArgTable = NULL;
   pUnclassifiedList = NULL;
}

/**
 *  @param [in] optWithArg -- list of options with an extra argument (Pass NULL if none)
 *
 *                            Should be passed in like this:
 *                            { _S("-c"),
 *                              _S("-f"),
 *                              _S("--longoption"),
 *                              NULL
 *                            }
 *
 *                            (Last element should be NULL to indicate end of list)
 *                            
 *
 */
CmdLineParser::CmdLineParser(int argc, char * argv[], pcwstr_t optWithArg[])
{
   cppmempool::MemPoolWrap memPool;

   progName = NULL;
   pArgTable = NULL;
   pUnclassifiedList = NULL;
   
   Parse(*__Convert(argc, argv), *__ConvertOptionsWithArg(optWithArg));
}

/**
 *  @param [in] optWithArg -- list of options with an extra argument (Pass NULL if none)
 */
CmdLineParser::CmdLineParser(int argc, pwstr_t argv[], pcwstr_t optWithArg[])
{
   cppmempool::MemPoolWrap memPool;

   progName = NULL;
   pArgTable = NULL;
   pUnclassifiedList = NULL;

   Parse(*__Convert(argc, argv), *__ConvertOptionsWithArg(optWithArg));
}

CmdLineParser::~CmdLineParser(void)
{
   cppmempool::Release(progName);
   cppmempool::Release(pArgTable);
   cppmempool::Release(pUnclassifiedList);
}



/**
 *  Parse arguments. 
 *
 *  It can recognize the following pattern:
 *  
 *  -c                   (short option on its own)
 *  --longoption         (long option on its own)
 *
 *  -f xxx               (short option which takes one argument)
 *  --longflag xxx       (long option which takes one argument)
 *
 *  -f xxx -f yyy                (same option which takes different arguments)
 *  -f xxx="123" -f yyy="456"    (same option which takes different key/value pairs)
 *
 *  -Dxxx                (short option with value attached to it)
 *  -Dxxx=yyy            (short option with key/value pairs attached to it)
 *  -Dxxx="yyy zzz eee"  (short option with key/value pairs attached to it and value part is embedded inside quotation marks)
 *
 */
void CmdLineParser::Parse(ArrayCppObjT<CFString> & argArray, HashtableT<CFString, CFString> & optWithArgTbl)
{
   this->pArgTable = new LinkedHashtableT<CFString, BaseObject>();
   cppmempool::Retain(this->pArgTable);

   // zero index is program name
   this->progName = new CFString(*argArray.Get(0));
   cppmempool::Retain(this->progName);


   CFString *emptyString = new CFString(_S("")); // for dummy use
   CFString *prevOpt = NULL;  // the previous option that take an argument
   for (int i = 1; i < static_cast<int>(argArray.GetSize()); ++i) {
      CFString *arg = argArray.Get(i);
      
      do {
         // previous option non-null
         if (prevOpt) {
            __AddOptToTable(*this->pArgTable, *prevOpt, *arg);
            prevOpt = NULL;
            break;
         }

         // options with one more argument, mark it for next round
         if (optWithArgTbl.ContainsKey(*arg)) {
            prevOpt = arg;
            break;
         }
         
         // long option
         if (arg->StartsWith(_S("--")) && arg->GetLength() > 2) {
            __AddOptToTable(*this->pArgTable, *arg, *emptyString);
            break;
         }

         // short option
         if (arg->StartsWith(_S("-")) && arg->GetLength() > 1) {
               
            if (arg->GetLength() == 2) {
               // short option only
               __AddOptToTable(*this->pArgTable, *arg, *emptyString);
            }
            else {
               // short option with key/value attached
               CFString *shortOpt = new CFString(arg->Left(2));
               CFString *optArg = new CFString(arg->Right(arg->GetLength() - 2));
               __AddOptToTable(*this->pArgTable, *shortOpt, *optArg);
            }

            break;
         }

         // unclassified, may be filenames, addresses or whatever
         AddUnclassified(arg);


      } while(false);
      
   }

}

/**
 *  Add unclassified object
 */
void CmdLineParser::AddUnclassified(CFString *pUnc)
{

   if (!this->pUnclassifiedList) {
      this->pUnclassifiedList = new ListCppObjT<CFString>();
      cppmempool::Retain(this->pUnclassifiedList);
   }

   this->pUnclassifiedList->AddTail(pUnc);
}

/**
 *  Get program name
 */
const CFString& CmdLineParser::GetProgramName()
{
   return *this->progName;
}

/**
 *  Get total distinct option count
 */
u32_t CmdLineParser::GetTotalOptionCount()
{
   return this->pArgTable->GetCount();
}

/**
 *  Check whether the requested option exists
 */
bool CmdLineParser::ContainsOption(pcwstr_t opt)
{
   CFString key(opt);
   return this->pArgTable->ContainsKey(key);
}

/**
 *  Get option by zero based index
 */
const CFString& CmdLineParser::GetOption(int index)
{
   const CFString *pKey = NULL;
   BaseObject *pVal = NULL;

   this->pArgTable->Get(index, pKey, pVal);
   return *pKey;
}

/**
 *  Get number of options with the same name
 */
u32_t CmdLineParser::GetOptionCount(pcwstr_t opt)
{
   CFString key(opt);
   BaseObject *pVal = this->pArgTable->Get(key);
   if (!pVal)
      return 0;

   if (cppflib::InstanceOf<NameValueList>(pVal))  // it is a list
      return static_cast<NameValueList*>(pVal)->GetCount();

   return 1; // value is string -> only one option
}

/**
 *  Get options of the same name
 *
 *  @param [in] opt -- option name
 *  @param [in] optIndex -- zero based index (from 0 to GetOptionCount() - 1)
 *  @param [out] name -- name of argument at 'optIndex' if any
 *  @param [out] val -- value of argument at 'optIndex' if any 
 */
bool CmdLineParser::GetOption(pcwstr_t opt, int optIndex, CFString& name, CFString& val)
{
   CFString key(opt);
   BaseObject *pVal = this->pArgTable->Get(key);
   if (!pVal) 
      return false;

   if (cppflib::InstanceOf<NameValueList>(pVal)) {  
      // it is a list
      NameValueList *pEmbeddedTable = static_cast<NameValueList*>(pVal);
      name = *pEmbeddedTable->GetKey(optIndex);
      val = *pEmbeddedTable->GetVal(optIndex);
   }
   else {  
      // only single one
      CFString *pName = static_cast<CFString*>(pVal);
      name = *pName;
      val = _S("");
   }

   return true;
}

/**
 *  Get number of unclassified command line arguments
 */
u32_t CmdLineParser::GetUnclassifiedCount()
{
   return pUnclassifiedList ? pUnclassifiedList->GetCount() : 0;
}

/**
 *  Get unclassified arguments by index
 *
 *  @param [in] index -- zero based index
 *
 *  @return the unclassified argument
 */
const CFString& CmdLineParser::GetUnclassified(int index)
{
   if (!pUnclassifiedList)
      throw exceptions::OutOfBoundsException(_S("Index out of bound"), -1);

   return *pUnclassifiedList->Get(index);
}



} // end of namespace util

} // end of namespace cppflib
